CLI Parameter Types
数据类型校验与转换
为 CLI parameter 添加类型提示后,Typer 会自动将参数转换成对应的数据类型
def main(name: str, age: int = 20, height_meters: float = 1.89, female: bool = True):
print(f"NAME is {name}, of type: {type(name)}")
print(f"--age is {age}, of type: {type(age)}")
print(f"--height-meters is {height_meters}, of type: {type(height_meters)}")
print(f"--female is {female}, of type: {type(female)}")
-
NAME会被当做str来看待 -
--age会被转换成int,--height-meters会被转换成float -
female是一个boolCLI option,所以--female是 True,--no-female则为 False
Number
使用 max and min 来为 int 或 float 类型添加范围限制
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
age: Annotated[int, typer.Option(min=18)] = 20,
score: Annotated[float, typer.Option(max=100)] = 0,
):
print(f"ID is {id}")
print(f"--age is {age}")
print(f"--score is {score}")
使用上下限而不是报错
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
rank: Annotated[int, typer.Option(max=10, clamp=True)] = 0,
score: Annotated[float, typer.Option(min=0, max=100, clamp=True)] = 0,
):
print(f"ID is {id}")
print(f"--rank is {rank}")
print(f"--score is {score}")
count 类型
You can make a CLI option work as a counter with the count parameter:
def main(verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0):
print(f"Verbose level is {verbose}")
--verbose 就变成了一个计数类型的参数了
Boolean 类型
bool 类型的 CLI Options,Typer 会默认使用形如 --something 和 --no-something 来表示
当然,我们也是可以自定义这一行为的
禁用否定形式
def main(force: Annotated[bool, typer.Option("--force")] = False):
自定义否定形式
在 typer.Option 中传入一个字符串,用 / 隔开,分别表示肯定形式和否定形式
def main(accept: Annotated[Optional[bool], typer.Option("--accept/--reject")] = None):
短名
let's say we want -f for --force and -F for --no-force:
def main(force: Annotated[bool, typer.Option("--force/--no-force", "-f/-F")] = False):
只要否定形式
那就别写肯定形式
注意 / 前有空格!
def main(in_prod: Annotated[bool, typer.Option(" /--demo", " /-d")] = True):
不过这样写的话,注释说明就很难看得懂了
UUID
你可以将 CLI Option 定义为 UUID
from uuid import UUID
import typer
def main(user_id: UUID):
print(f"USER_ID is {user_id}")
print(f"UUID version is: {user_id.version}")
if __name__ == "__main__":
typer.run(main)
DateTime
from datetime import datetime
import typer
def main(birth: datetime):
print(f"Interesting day to be born: {birth}")
print(f"Birth hour: {birth.hour}")
if __name__ == "__main__":
typer.run(main)
Typer will accept any string from the following formats:
%Y-%m-%d%Y-%m-%dT%H:%M:%S%Y-%m-%d %H:%M:%S
自定义日期字符串格式
def main(
launch_date: Annotated[
datetime,
typer.Argument(
formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"]
),
],
):
print(f"Launch will be at: {launch_date}")
Enum
from enum import Enum
import typer
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
def main(network: NeuralNetwork = NeuralNetwork.simple):
print(f"Training neural network of type: {network.value}")
if __name__ == "__main__":
typer.run(main)
Notice that the function parameter
networkwill be anEnum, not astr.To get the
strvalue in your function's code usenetwork.value.
大小写敏感
默认情况下,Enum 类型的 CLI Parameter 是大小写敏感的,可以用 case_sensitive 来控制
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
def main(
network: Annotated[
NeuralNetwork, typer.Option(case_sensitive=False)
] = NeuralNetwork.simple,
):
print(f"Training neural network of type: {network.value}")
Enum 列表
A CLI parameter can also take a list of Enum values:
class Food(str, Enum):
food_1 = "Eggs"
food_2 = "Bacon"
food_3 = "Cheese"
def main(groceries: Annotated[List[Food], typer.Option()] = [Food.food_1, Food.food_3]):
print(f"Buying groceries: {', '.join([f.value for f in groceries])}")
Path
You can declare a CLI parameter to be a standard Python pathlib.Path.
This is what you would do for directory paths, file paths, etc:
from pathlib import Path
from typing import Optional
import typer
from typing_extensions import Annotated
def main(config: Annotated[Optional[Path], typer.Option()] = None):
if config is None:
print("No config file")
raise typer.Abort()
if config.is_file():
text = config.read_text()
print(f"Config file contents: {text}")
elif config.is_dir():
print("Config is a directory, will use all its config files")
elif not config.exists():
print("The config doesn't exist")
if __name__ == "__main__":
typer.run(main)
当然会有自动补全啦
校验选项
exists: if set to true, 文件或目录必须得存在. If this is false 而且文件确实不存在,那之后的校验都不会进行了file_okay: 校验是否是文件dir_okay: 校验是否是目录writable: if true, 会检查是否可写readable: if true, 会检查是否可读resolve_path: if this is true, 文件会被转换为绝对路径(软连接也会哦)- 带有
~的路径不会被展开
- 带有
from pathlib import Path
import typer
from typing_extensions import Annotated
def main(
config: Annotated[
Path,
typer.Option(
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
),
],
):
text = config.read_text()
print(f"Config file contents: {text}")
if __name__ == "__main__":
typer.run(main)
File
允许 CLI Parameters 以文件形式读 / 写文件
详见:https://typer.tiangolo.com/tutorial/parameter-types/file/
Custom Types
两种方式,使 Typer 支持你自定义的类:
- 定义
parser函数来解析数据 - 定义 Click's custom types
import typer
from typing_extensions import Annotated
class CustomClass:
def __init__(self, value: str):
self.value = value
def __str__(self):
return f"<CustomClass: value={self.value}>"
def parse_custom_class(value: str):
return CustomClass(value * 2)
def main(
custom_arg: Annotated[CustomClass, typer.Argument(parser=parse_custom_class)],
custom_opt: Annotated[CustomClass, typer.Option(parser=parse_custom_class)] = "Foo",
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")
if __name__ == "__main__":
typer.run(main)
import click
import typer
from typing_extensions import Annotated
class CustomClass:
def __init__(self, value: str):
self.value = value
def __repr__(self):
return f"<CustomClass: value={self.value}>"
class CustomClassParser(click.ParamType):
name = "CustomClass"
def convert(self, value, param, ctx):
return CustomClass(value * 3)
def main(
custom_arg: Annotated[CustomClass, typer.Argument(click_type=CustomClassParser())],
custom_opt: Annotated[
CustomClass, typer.Option(click_type=CustomClassParser())
] = "Foo",
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")
if __name__ == "__main__":
typer.run(main)